import { DefaultProxy } from '../../Source/Cesium.js';
import { defaultValue } from '../../Source/Cesium.js';
import { queryToObject } from '../../Source/Cesium.js';
import { Request } from '../../Source/Cesium.js';
import { RequestErrorEvent } from '../../Source/Cesium.js';
import { RequestScheduler } from '../../Source/Cesium.js';
import { Resource } from '../../Source/Cesium.js';
import createCanvas from '../createCanvas.js';
import { Uri } from '../../Source/Cesium.js';
import { when } from '../../Source/Cesium.js';

describe('Core/Resource', function() {

    var dataUri = '';
    var supportsImageBitmapOptions;

    beforeAll(function() {
        return Resource.supportsImageBitmapOptions()
            .then(function(result) {
                supportsImageBitmapOptions = result;
            });
    });

    it('Constructor sets correct properties', function() {
        var proxy = new DefaultProxy('/proxy/');
        var request = new Request();
        function retryFunc() {
        }

        var resource = new Resource({
            url: 'http://test.com/tileset',
            queryParameters: {
                key1: 'value1',
                key2: 'value2'
            },
            templateValues: {
                key3: 'value3',
                key4: 'value4'
            },
            headers: {
                Accept: 'application/test-type'
            },
            proxy: proxy,
            retryCallback: retryFunc,
            retryAttempts: 4,
            request: request
        });

        expect(resource.getUrlComponent(false, false)).toEqual('http://test.com/tileset');
        expect(resource.getUrlComponent(true, false)).toEqual('http://test.com/tileset?key1=value1&key2=value2');
        expect(resource.getUrlComponent(false, true)).toEqual(proxy.getURL('http://test.com/tileset'));
        expect(resource.getUrlComponent(true, true)).toEqual(proxy.getURL('http://test.com/tileset?key1=value1&key2=value2'));
        expect(resource.url).toEqual(proxy.getURL('http://test.com/tileset?key1=value1&key2=value2'));
        expect(resource.queryParameters).toEqual({
            key1: 'value1',
            key2: 'value2'
        });
        expect(resource.templateValues).toEqual({
            key3: 'value3',
            key4: 'value4'
        });
        expect(resource.headers).toEqual({
            Accept: 'application/test-type'
        });
        expect(resource.proxy).toBe(proxy);
        expect(resource.retryCallback).toBe(retryFunc);
        expect(resource.retryAttempts).toEqual(4);
        expect(resource._retryCount).toEqual(0);
        expect(resource.request).toBe(request);
    });

    it('Constructor sets correct properties', function() {
        var url = 'http://invalid.domain.com/tileset';
        var resource = new Resource(url);
        expect(resource.url).toEqual(url);
        expect(resource.queryParameters).toEqual({});
        expect(resource.templateValues).toEqual({});
        expect(resource.headers).toEqual({});
        expect(resource.proxy).toBeUndefined();
        expect(resource.retryCallback).toBeUndefined();
        expect(resource.retryAttempts).toEqual(0);
        expect(resource.request).toBeDefined();
    });

    it('_makeRequest returns undefined if the request is throttled', function() {
        var oldMaximumRequests = RequestScheduler.maximumRequests;
        RequestScheduler.maximumRequests = 0;

        var resource = new Resource({
            url: 'http://example.invalid/testuri',
            request: new Request({
                throttle: true
            })
        });
        var promise = resource._makeRequest({ method: 'GET' });
        expect(promise).toBeUndefined();

        RequestScheduler.maximumRequests = oldMaximumRequests;
    });

    it('appendForwardSlash appends a /', function() {
        var resource = new Resource({
            url: 'http://test.com/tileset'
        });
        expect(resource.url).toEqual('http://test.com/tileset');
        resource.appendForwardSlash();
        expect(resource.url).toEqual('http://test.com/tileset/');
    });

    it('Setting a url with a query string sets queryParameters correctly', function() {
        var resource = new Resource({
            url: 'http://test.com/tileset?foo=bar&baz=foo'
        });
        expect(resource.getUrlComponent()).toEqual('http://test.com/tileset');
        expect(resource.getUrlComponent(true)).toEqual('http://test.com/tileset?foo=bar&baz=foo');
        expect(resource.queryParameters).toEqual({
            foo: 'bar',
            baz: 'foo'
        });
    });

    it('createIfNeeded returns undefined, if parameter is undefined', function() {
        expect(Resource.createIfNeeded()).toBeUndefined();
    });

    it('createIfNeeded returns Resource, if parameter is a Resource', function() {
        var resource = new Resource({
            url: 'http://test.com/tileset'
        });
        expect(Resource.createIfNeeded(resource)).toEqual(resource);
    });

    it('createIfNeeded returns Resource, if parameter is a String', function() {
        var resource = Resource.createIfNeeded('http://test.com/tileset');
        expect(resource.url).toEqual('http://test.com/tileset');
    });

    it('multiple values for query parameters are allowed', function() {
        var resource = new Resource('http://test.com/tileset/endpoint?a=1&a=2&b=3&a=4');
        expect(resource.queryParameters.a).toEqual(['1', '2', '4']);
        expect(resource.queryParameters.b).toEqual('3');

        expect(resource.url).toEqual('http://test.com/tileset/endpoint?a=1&a=2&a=4&b=3');
    });

    it('multiple values for query parameters works with getDerivedResource without preserverQueryParameters', function() {
        var resource = new Resource('http://test.com/tileset/endpoint?a=1&a=2&b=3&a=4');
        expect(resource.queryParameters.a).toEqual(['1', '2', '4']);
        expect(resource.queryParameters.b).toEqual('3');

        expect(resource.url).toEqual('http://test.com/tileset/endpoint?a=1&a=2&a=4&b=3');

        var derived = resource.getDerivedResource({
            url: 'other_endpoint?a=5&b=6&a=7'
        });

        expect(derived.queryParameters.a).toEqual(['5', '7']);
        expect(derived.queryParameters.b).toEqual('6');

        expect(derived.url).toEqual('http://test.com/tileset/other_endpoint?a=5&a=7&b=6');
    });

    it('multiple values for query parameters works with getDerivedResource with preserveQueryParameters', function() {
        var resource = new Resource('http://test.com/tileset/endpoint?a=1&a=2&b=3&a=4');
        expect(resource.queryParameters.a).toEqual(['1', '2', '4']);
        expect(resource.queryParameters.b).toEqual('3');

        expect(resource.url).toEqual('http://test.com/tileset/endpoint?a=1&a=2&a=4&b=3');

        var derived = resource.getDerivedResource({
            url: 'other_endpoint?a=5&b=6&a=7',
            preserveQueryParameters: true
        });

        expect(derived.queryParameters.a).toEqual(['5', '7', '1', '2', '4']);
        expect(derived.queryParameters.b).toEqual(['6', '3']);

        expect(derived.url).toEqual('http://test.com/tileset/other_endpoint?a=5&a=7&a=1&a=2&a=4&b=6&b=3');
    });

    it('replaces templateValues in the url', function() {
        var resource = new Resource({
            url: 'http://test.com/tileset/{foo}/{bar}',
            templateValues: {
                foo: 'test1',
                bar: 'test2'
            }
        });

        expect(resource.url).toEqual('http://test.com/tileset/test1/test2');
    });

    it('replaces numeric templateValues', function() {
        var resource = new Resource({
            url: 'http://test.com/tileset/{0}/{1}',
            templateValues: {
                '0': 'test1',
                '1': 'test2'
            }
        });

        expect(resource.url).toEqual('http://test.com/tileset/test1/test2');
    });

    it('leaves templateValues unchanged that are not provided', function() {
        var resource = new Resource({
            url: 'http://test.com/tileset/{foo}/{bar}'
        });

        expect(resource.url).toEqual('http://test.com/tileset/{foo}/{bar}');
    });

    it('url encodes replacement templateValues in the url', function() {
        var resource = new Resource({
            url: 'http://test.com/tileset/{foo}/{bar}',
            templateValues: {
                foo: 'a/b',
                bar: 'x$y#'
            }
        });

        expect(resource.url).toEqual('http://test.com/tileset/a%2Fb/x%24y%23');
    });

    it('getDerivedResource sets correct properties', function() {
        var proxy = new DefaultProxy('/proxy/');
        var request = new Request();
        function retryFunc() {
        }

        var parent = new Resource({
            url: 'http://test.com/tileset?key=value',
            queryParameters: {
                foo: 'bar'
            },
            templateValues: {
                key5: 'value5',
                key6: 'value6'
            }
        });
        parent.appendForwardSlash();

        var resource = parent.getDerivedResource({
            url: 'tileset.json',
            queryParameters: {
                key1: 'value1',
                key2: 'value2'
            },
            templateValues: {
                key3: 'value3',
                key4: 'value4'
            },
            headers: {
                Accept: 'application/test-type'
            },
            proxy: proxy,
            retryCallback: retryFunc,
            retryAttempts: 4,
            request: request
        });

        expect(resource.getUrlComponent(false, false)).toEqual('http://test.com/tileset/tileset.json');
        expect(resource.getUrlComponent(true, false)).toEqual('http://test.com/tileset/tileset.json?key1=value1&key2=value2&key=value&foo=bar');
        expect(resource.getUrlComponent(false, true)).toEqual(proxy.getURL('http://test.com/tileset/tileset.json'));
        expect(resource.getUrlComponent(true, true)).toEqual(proxy.getURL('http://test.com/tileset/tileset.json?key1=value1&key2=value2&key=value&foo=bar'));
        expect(resource.url).toEqual(proxy.getURL('http://test.com/tileset/tileset.json?key1=value1&key2=value2&key=value&foo=bar'));
        expect(resource.queryParameters).toEqual({
            foo: 'bar',
            key: 'value',
            key1: 'value1',
            key2: 'value2'
        });
        expect(resource.templateValues).toEqual({
            key5: 'value5',
            key6: 'value6',
            key3: 'value3',
            key4: 'value4'
        });
        expect(resource.headers).toEqual({
            Accept: 'application/test-type'
        });
        expect(resource.proxy).toBe(proxy);
        expect(resource.retryCallback).toBe(retryFunc);
        expect(resource.retryAttempts).toEqual(4);
        expect(resource._retryCount).toEqual(0);
        expect(resource.request).toBe(request);
    });

    it('getDerivedResource works with directory parent resource', function() {
        var parent = new Resource({
            url: 'http://test.com/tileset/'
        });

        expect(parent.url).toEqual('http://test.com/tileset/');

        var resource = parent.getDerivedResource({
            url: 'tileset.json'
        });

        expect(resource.url).toEqual('http://test.com/tileset/tileset.json');
    });

    it('getDerivedResource works with file parent resource', function() {
        var parent = new Resource({
            url: 'http://test.com/tileset/tileset.json'
        });

        expect(parent.url).toEqual('http://test.com/tileset/tileset.json');

        var resource = parent.getDerivedResource({
            url: '0/0/0.b3dm'
        });

        expect(resource.url).toEqual('http://test.com/tileset/0/0/0.b3dm');
    });

    it('getDerivedResource works with only template values', function() {
        var parent = new Resource({
            url: 'http://test.com/terrain/{z}/{x}/{y}.terrain'
        });

        expect(parent.url).toEqual('http://test.com/terrain/{z}/{x}/{y}.terrain');

        var resource = parent.getDerivedResource({
            templateValues: {
                x: 1,
                y: 2,
                z: 0
            }
        });

        expect(resource.url).toEqual('http://test.com/terrain/0/1/2.terrain');
    });

    it('getDerivedResource works with only query parameters', function() {
        var parent = new Resource({
            url: 'http://test.com/terrain'
        });

        expect(parent.url).toEqual('http://test.com/terrain');

        var resource = parent.getDerivedResource({
            queryParameters: {
                x: 1,
                y: 2,
                z: 0
            }
        });

        expect(resource.url).toEqual('http://test.com/terrain?x=1&y=2&z=0');
    });

    it('setQueryParameters with useAsDefault set to true', function() {
        var resource = new Resource({
            url: 'http://test.com/terrain',
            queryParameters: {
                x: 1,
                y: 2
            }
        });

        expect(resource.queryParameters).toEqual({
            x: 1,
            y: 2
        });

        resource.setQueryParameters({
            x: 3,
            y: 4,
            z: 0
        }, true);

        expect(resource.queryParameters).toEqual({
            x: 1,
            y: 2,
            z: 0
        });
    });

    it('setQueryParameters with useAsDefault set to false', function() {
        var resource = new Resource({
            url: 'http://test.com/terrain',
            queryParameters: {
                x: 1,
                y: 2
            }
        });

        expect(resource.queryParameters).toEqual({
            x: 1,
            y: 2
        });

        resource.setQueryParameters({
            x: 3,
            y: 4,
            z: 0
        }, false);

        expect(resource.queryParameters).toEqual({
            x: 3,
            y: 4,
            z: 0
        });
    });

    it('appendQueryParameters works with non-arrays', function() {
        var resource = new Resource({
            url: 'http://test.com/terrain',
            queryParameters: {
                x: 1,
                y: 2
            }
        });

        expect(resource.queryParameters).toEqual({
            x: 1,
            y: 2
        });

        resource.appendQueryParameters({
            x: 3,
            y: 4,
            z: 0
        });

        expect(resource.queryParameters).toEqual({
            x: [3, 1],
            y: [4, 2],
            z: 0
        });
    });

    it('appendQueryParameters works with arrays/non-arrays', function() {
        var resource = new Resource({
            url: 'http://test.com/terrain',
            queryParameters: {
                x: [1, 2],
                y: 2,
                z: [-1, -2]
            }
        });

        expect(resource.queryParameters).toEqual({
            x: [1, 2],
            y: 2,
            z: [-1, -2]
        });

        resource.appendQueryParameters({
            x: 3,
            y: [4, 5],
            z: [-3, -4]
        });

        expect(resource.queryParameters).toEqual({
            x: [3, 1, 2],
            y: [4, 5, 2],
            z: [-3, -4, -1, -2]
        });
    });

    it('setTemplateValues with useAsDefault set to true', function() {
        var resource = new Resource({
            url: 'http://test.com/terrain/{z}/{x}/{y}.terrain',
            templateValues: {
                x: 1,
                y: 2,
                map: 'my map'
            }
        });

        expect(resource.templateValues).toEqual({
            x: 1,
            y: 2,
            map: 'my map'
        });

        resource.setTemplateValues({
            x: 3,
            y: 4,
            z: 0,
            style: 'my style'
        }, true);

        expect(resource.templateValues).toEqual({
            x: 1,
            y: 2,
            map: 'my map',
            z: 0,
            style: 'my style'
        });
    });

    it('setTemplateValues with useAsDefault set to false', function() {
        var resource = new Resource({
            url: 'http://test.com/terrain/{z}/{x}/{y}.terrain',
            templateValues: {
                x: 1,
                y: 2,
                map: 'my map'
            }
        });

        expect(resource.templateValues).toEqual({
            x: 1,
            y: 2,
            map: 'my map'
        });

        resource.setTemplateValues({
            x: 3,
            y: 4,
            z: 0,
            style: 'my style'
        }, false);

        expect(resource.templateValues).toEqual({
            x: 3,
            y: 4,
            map: 'my map',
            z: 0,
            style: 'my style'
        });
    });

    it('retryOnFail doesn\'t exceed retryAttempts', function() {
        var cb = jasmine.createSpy('retry').and.returnValue(true);
        var resource = new Resource({
            url: 'http://test.com/terrain',
            retryCallback: cb,
            retryAttempts: 3
        });

        var promises = [];
        for (var i=0;i<6;++i) {
            promises.push(resource.retryOnError());
        }

        when.all(promises)
            .then(function(result) {
                expect(result).toEqual([true, true, true, false, false, false]);
                expect(cb.calls.count()).toEqual(3);
                expect(resource._retryCount).toEqual(3);
            });
    });

    it('retryOnFail returns value from callback', function() {
        var result = true;
        var cb = jasmine.createSpy('retry').and.callFake(function() {
            result = !result;
            return result;
        });

        var resource = new Resource({
            url: 'http://test.com/terrain',
            retryCallback: cb,
            retryAttempts: 4
        });

        var promises = [];
        for (var i=0;i<6;++i) {
            promises.push(resource.retryOnError());
        }

        when.all(promises)
            .then(function(result) {
                expect(result).toEqual([false, true, false, true, false, false]);
                expect(cb.calls.count()).toEqual(4);
                expect(resource._retryCount).toEqual(4);
            });
    });

    it('isDataUri returns correct values', function() {
        var dataResource = new Resource({
            url: 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3'
        });

        expect(dataResource.isDataUri).toBe(true);

        var resource = new Resource({
            url: 'http://invalid.uri/tileset'
        });

        expect(resource.isDataUri).toBe(false);
    });

    it('isBlobUri returns correct values', function() {
        var dataResource = new Resource({
            url: 'blob:d3958f5c-0777-0845-9dcf-2cb28783acaf'
        });

        expect(dataResource.isBlobUri).toBe(true);

        var resource = new Resource({
            url: 'http://invalid.uri/tileset'
        });

        expect(resource.isBlobUri).toBe(false);
    });

    it('post calls with correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResponseType = 'json';
        var expectedData = {
            stuff: 'myStuff'
        };
        var expectedHeaders = {
            'X-My-Header': 'My-Value'
        };
        var expectedResult = {
            status: 'success'
        };
        var expectedMimeType = 'application/test-data';
        var resource = new Resource({
            url: expectedUrl,
            headers: expectedHeaders
        });

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(responseType).toEqual(expectedResponseType);
            expect(method).toEqual('POST');
            expect(data).toEqual(expectedData);
            expect(headers['X-My-Header']).toEqual('My-Value');
            expect(headers['X-My-Other-Header']).toEqual('My-Other-Value');
            expect(overrideMimeType).toBe(expectedMimeType);
            deferred.resolve(expectedResult);
        });

        return resource.post(expectedData, {
            responseType: expectedResponseType,
            headers: {
                'X-My-Other-Header': 'My-Other-Value'
            },
            overrideMimeType: expectedMimeType
        })
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('static post calls with correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResponseType = 'json';
        var expectedData = {
            stuff: 'myStuff'
        };
        var expectedHeaders = {
            'X-My-Header': 'My-Value'
        };
        var expectedResult = {
            status: 'success'
        };
        var expectedMimeType = 'application/test-data';

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(responseType).toEqual(expectedResponseType);
            expect(method).toEqual('POST');
            expect(data).toEqual(expectedData);
            expect(headers).toEqual(expectedHeaders);
            expect(overrideMimeType).toBe(expectedMimeType);
            deferred.resolve(expectedResult);
        });

        return Resource.post({
            url: expectedUrl,
            data: expectedData,
            responseType: expectedResponseType,
            headers: expectedHeaders,
            overrideMimeType: expectedMimeType
        })
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('put calls with correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResponseType = 'json';
        var expectedData = {
            stuff: 'myStuff'
        };
        var expectedHeaders = {
            'X-My-Header': 'My-Value'
        };
        var expectedResult = {
            status: 'success'
        };
        var expectedMimeType = 'application/test-data';
        var resource = new Resource({
            url: expectedUrl,
            headers: expectedHeaders
        });

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(responseType).toEqual(expectedResponseType);
            expect(method).toEqual('PUT');
            expect(data).toEqual(expectedData);
            expect(headers['X-My-Header']).toEqual('My-Value');
            expect(headers['X-My-Other-Header']).toEqual('My-Other-Value');
            expect(overrideMimeType).toBe(expectedMimeType);
            deferred.resolve(expectedResult);
        });

        return resource.put(expectedData, {
            responseType: expectedResponseType,
            headers: {
                'X-My-Other-Header': 'My-Other-Value'
            },
            overrideMimeType: expectedMimeType
        })
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('static put calls with correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResponseType = 'json';
        var expectedData = {
            stuff: 'myStuff'
        };
        var expectedHeaders = {
            'X-My-Header': 'My-Value'
        };
        var expectedResult = {
            status: 'success'
        };
        var expectedMimeType = 'application/test-data';

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(responseType).toEqual(expectedResponseType);
            expect(method).toEqual('PUT');
            expect(data).toEqual(expectedData);
            expect(headers).toEqual(expectedHeaders);
            expect(overrideMimeType).toBe(expectedMimeType);
            deferred.resolve(expectedResult);
        });

        return Resource.put({
            url: expectedUrl,
            data: expectedData,
            responseType: expectedResponseType,
            headers: expectedHeaders,
            overrideMimeType: expectedMimeType
        })
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('patch calls with correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResponseType = 'json';
        var expectedData = {
            stuff: 'myStuff'
        };
        var expectedHeaders = {
            'X-My-Header': 'My-Value'
        };
        var expectedResult = {
            status: 'success'
        };
        var expectedMimeType = 'application/test-data';
        var resource = new Resource({
            url: expectedUrl,
            headers: expectedHeaders
        });

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(responseType).toEqual(expectedResponseType);
            expect(method).toEqual('PATCH');
            expect(data).toEqual(expectedData);
            expect(headers['X-My-Header']).toEqual('My-Value');
            expect(headers['X-My-Other-Header']).toEqual('My-Other-Value');
            expect(overrideMimeType).toBe(expectedMimeType);
            deferred.resolve(expectedResult);
        });

        return resource.patch(expectedData, {
            responseType: expectedResponseType,
            headers: {
                'X-My-Other-Header': 'My-Other-Value'
            },
            overrideMimeType: expectedMimeType
        })
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('static patch calls with correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResponseType = 'json';
        var expectedData = {
            stuff: 'myStuff'
        };
        var expectedHeaders = {
            'X-My-Header': 'My-Value'
        };
        var expectedResult = {
            status: 'success'
        };
        var expectedMimeType = 'application/test-data';

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(responseType).toEqual(expectedResponseType);
            expect(method).toEqual('PATCH');
            expect(data).toEqual(expectedData);
            expect(headers).toEqual(expectedHeaders);
            expect(overrideMimeType).toBe(expectedMimeType);
            deferred.resolve(expectedResult);
        });

        return Resource.patch({
            url: expectedUrl,
            data: expectedData,
            responseType: expectedResponseType,
            headers: expectedHeaders,
            overrideMimeType: expectedMimeType
        })
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('static fetchArrayBuffer calls correct method', function() {
        var url = 'http://test.com/data';
        var expectedResult = when.resolve();
        spyOn(Resource.prototype, 'fetchArrayBuffer').and.returnValue(expectedResult);
        var result = Resource.fetchArrayBuffer(url);
        expect(result).toBe(expectedResult);
        return Resource.fetchArrayBuffer(url)
            .then(function() {
                expect(Resource.prototype.fetchArrayBuffer).toHaveBeenCalled();
            });
    });

    it('fetchArrayBuffer calls fetch with expected parameters', function() {
        var url = 'http://test.com/data';
        var expectedResult = when.resolve();
        spyOn(Resource.prototype, 'fetch').and.returnValue(expectedResult);
        var result = Resource.fetchArrayBuffer(url);
        expect(result).toBe(expectedResult);
        return result
            .then(function() {
                expect(Resource.prototype.fetch).toHaveBeenCalledWith({ responseType: 'arraybuffer' });
            });
    });

    it('static fetchBlob calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'fetchBlob').and.returnValue(when.resolve());
        return Resource.fetchBlob(url)
            .then(function() {
                expect(Resource.prototype.fetchBlob).toHaveBeenCalled();
            });
    });

    it('fetchBlob calls fetch with expected parameters', function() {
        var url = 'http://test.com/data';
        var expectedResult = when.resolve();
        spyOn(Resource.prototype, 'fetch').and.returnValue(expectedResult);
        var result = Resource.fetchBlob(url);
        expect(result).toBe(expectedResult);
        return result
            .then(function() {
                expect(Resource.prototype.fetch).toHaveBeenCalledWith({ responseType: 'blob' });
            });
    });

    it('fetchArrayBuffer calls fetch with expected parameters', function() {
        var url = 'http://test.com/data';
        var expectedResult = when.resolve();
        spyOn(Resource.prototype, 'fetch').and.returnValue(expectedResult);
        var result = Resource.fetchArrayBuffer(url);
        expect(result).toBe(expectedResult);
        return result
            .then(function() {
                expect(Resource.prototype.fetch).toHaveBeenCalledWith({ responseType: 'arraybuffer' });
            });
    });

    it('static fetchImage calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'fetchImage').and.returnValue(when.resolve());
        return Resource.fetchImage(url)
            .then(function() {
                expect(Resource.prototype.fetchImage).toHaveBeenCalled();
            });
    });

    it('static fetchText calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'fetchText').and.returnValue(when.resolve());
        return Resource.fetchText(url)
            .then(function() {
                expect(Resource.prototype.fetchText).toHaveBeenCalled();
            });
    });

    it('fetchText calls fetch with expected parameters', function() {
        var url = 'http://test.com/data';
        var expectedResult = when.resolve();
        spyOn(Resource.prototype, 'fetch').and.returnValue(expectedResult);
        var result = Resource.fetchText(url);
        expect(result).toBe(expectedResult);
        return result
            .then(function() {
                expect(Resource.prototype.fetch).toHaveBeenCalledWith({ responseType: 'text' });
            });
    });

    it('static fetchJson calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'fetchJson').and.returnValue(when.resolve());
        return Resource.fetchJson(url)
            .then(function() {
                expect(Resource.prototype.fetchJson).toHaveBeenCalled();
            });
    });

    it('fetchJson calls fetch with expected parameters and parses result', function() {
        var expectedResult = { x: 123 };
        spyOn(Resource.prototype, 'fetch').and.returnValue(when.resolve(JSON.stringify(expectedResult)));
        return Resource.fetchJson('url')
            .then(function(result) {
                expect(result).toEqual(expectedResult);
                expect(Resource.prototype.fetch).toHaveBeenCalledWith({
                    responseType: 'text',
                    headers: {
                        Accept: 'application/json,*/*;q=0.01'
                    }
                });
            });
    });

    it('static fetchXML calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'fetchXML').and.returnValue(when.resolve());
        return Resource.fetchXML(url)
            .then(function() {
                expect(Resource.prototype.fetchXML).toHaveBeenCalled();
            });
    });

    it('fetchXML calls fetch with expected parameters', function() {
        var url = 'http://test.com/data';
        var expectedResult = when.resolve();
        spyOn(Resource.prototype, 'fetch').and.returnValue(expectedResult);
        var result = Resource.fetchXML(url);
        expect(result).toBe(expectedResult);
        return result
            .then(function() {
                expect(Resource.prototype.fetch).toHaveBeenCalledWith({
                    responseType: 'document',
                    overrideMimeType: 'text/xml'
                });
            });
    });

    it('static fetchJsonp calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'fetchJsonp').and.returnValue(when.resolve());
        return Resource.fetchJsonp(url)
            .then(function() {
                expect(Resource.prototype.fetchJsonp).toHaveBeenCalled();
            });
    });

    it('static fetch calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'fetch').and.returnValue(when.resolve());
        return Resource.fetch(url)
            .then(function() {
                expect(Resource.prototype.fetch).toHaveBeenCalled();
            });
    });

    it('fetch calls correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResult = {
            status: 'success'
        };

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(method).toEqual('GET');
            deferred.resolve(expectedResult);
        });

        var resource = new Resource({url: expectedUrl});
        return resource.fetch()
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('static delete calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'delete').and.returnValue(when.resolve());
        return Resource.delete(url)
            .then(function() {
                expect(Resource.prototype.delete).toHaveBeenCalled();
            });
    });

    it('delete calls correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResult = {
            status: 'success'
        };

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(method).toEqual('DELETE');
            deferred.resolve(expectedResult);
        });

        var resource = new Resource({url: expectedUrl});
        return resource.delete()
            .then(function(result) {
                expect(result).toEqual(expectedResult);
            });
    });

    it('static head calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'head').and.returnValue(when.resolve({}));
        return Resource.head(url)
            .then(function() {
                expect(Resource.prototype.head).toHaveBeenCalled();
            });
    });

    it('head calls correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResult = {
            'accept-ranges': 'bytes',
            'access-control-allow-headers' : 'Origin, X-Requested-With, Content-Type, Accept',
            'access-control-allow-origin' : '*',
            'cache-control': 'public, max-age=0',
            'connection' : 'keep-alive',
            'content-length' : '883',
            'content-type' : 'image/png',
            'date' : 'Tue, 13 Feb 2018 03:38:55 GMT',
            'etag' : 'W/"373-15e34d146a1"',
            'vary' : 'Accept-Encoding',
            'x-powered-vy' : 'Express'
        };
        var headerString = '';
        for (var key in expectedResult) {
            if (expectedResult.hasOwnProperty(key)) {
                headerString += key + ': ' + expectedResult[key] + '\r\n';
            }
        }
        var fakeXHR = {
            status: 200,
            send : function() {
                this.onload();
            },
            open : function() {},
            getAllResponseHeaders : function() {
                return headerString;
            }
        };
        spyOn(window, 'XMLHttpRequest').and.returnValue(fakeXHR);

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(method).toEqual('HEAD');
            Resource._DefaultImplementations.loadWithXhr(url, responseType, method, data, headers, deferred, overrideMimeType);
        });

        var resource = new Resource({url: expectedUrl});
        return resource.head()
            .then(function(result) {
                expect(result.date).toEqual(expectedResult.date);
                expect(result['last-modified']).toEqual(expectedResult['last-modified']);
                expect(result['x-powered-by']).toEqual(expectedResult['x-powered-by']);
                expect(result.etag).toEqual(expectedResult.etag);
                expect(result['content-type']).toEqual(expectedResult['content-type']);
                expect(result['access-control-allow-origin']).toEqual(expectedResult['access-control-allow-origin']);
                expect(result['cache-control']).toEqual(expectedResult['cache-control']);
                expect(result['accept-ranges']).toEqual(expectedResult['accept-ranges']);
                expect(result['access-control-allow-headers']).toEqual(expectedResult['access-control-allow-headers']);
                expect(result['content-length']).toEqual(expectedResult['content-length']);
            });
    });

    it('static options calls correct method', function() {
        var url = 'http://test.com/data';
        spyOn(Resource.prototype, 'options').and.returnValue(when.resolve({}));
        return Resource.options(url)
            .then(function() {
                expect(Resource.prototype.options).toHaveBeenCalled();
            });
    });

    it('options calls correct method', function() {
        var expectedUrl = 'http://test.com/endpoint';
        var expectedResult = {
            'access-control-allow-headers' : 'Origin, X-Requested-With, Content-Type, Accept',
            'access-control-allow-methods' : 'GET, PUT, POST, DELETE, OPTIONS',
            'access-control-allow-origin' : '*',
            'connection' : 'keep-alive',
            'content-length' : '2',
            'content-type' : 'text/plain; charset=utf-8',
            'date' : 'Tue, 13 Feb 2018 03:38:55 GMT',
            'etag' : 'W/"2-nOO9QiTIwXgNtWtBJezz8kv3SLc"',
            'vary' : 'Accept-Encoding',
            'x-powered-vy' : 'Express'
        };
        var headerString = '';
        for (var key in expectedResult) {
            if (expectedResult.hasOwnProperty(key)) {
                headerString += key + ': ' + expectedResult[key] + '\r\n';
            }
        }
        var fakeXHR = {
            status: 200,
            send : function() {
                this.onload();
            },
            open : function() {},
            getAllResponseHeaders : function() {
                return headerString;
            }
        };
        spyOn(window, 'XMLHttpRequest').and.returnValue(fakeXHR);

        spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
            expect(url).toEqual(expectedUrl);
            expect(method).toEqual('OPTIONS');
            Resource._DefaultImplementations.loadWithXhr(url, responseType, method, data, headers, deferred, overrideMimeType);
        });

        var resource = new Resource({url: expectedUrl});
        return resource.options()
            .then(function(result) {
                expect(result.date).toEqual(expectedResult.date);
                expect(result['x-powered-by']).toEqual(expectedResult['x-powered-by']);
                expect(result.etag).toEqual(expectedResult.etag);
                expect(result['content-type']).toEqual(expectedResult['content-type']);
                expect(result['access-control-allow-origin']).toEqual(expectedResult['access-control-allow-origin']);
                expect(result['access-control-allow-methods']).toEqual(expectedResult['access-control-allow-methods']);
                expect(result['access-control-allow-headers']).toEqual(expectedResult['access-control-allow-headers']);
                expect(result['content-length']).toEqual(expectedResult['content-length']);
            });
    });

    it('can load an SVG', function() {
        return Resource.fetchImage('./Data/Images/Red16x16.svg').then(function(loadedImage) {
            expect(loadedImage.width).toEqual(16);
            expect(loadedImage.height).toEqual(16);
        });
    });

    it('can load an image preferring blob', function() {
        return Resource.fetchImage('./Data/Images/Green.png', true).then(function(loadedImage) {
            expect(loadedImage.width).toEqual(1);
            expect(loadedImage.height).toEqual(1);
        });
    });

    it('can load an image from a data URI', function() {
        return Resource.fetchImage(dataUri).then(function(loadedImage) {
            expect(loadedImage.width).toEqual(1);
            expect(loadedImage.height).toEqual(1);
        });
    });

    describe('fetchImage with ImageBitmap', function() {
        var canvas;
        beforeAll(function() {
            canvas = createCanvas(1, 2);
        });

        afterAll(function() {
            document.body.removeChild(canvas);
        });

        function getColorAtPixel(image, x, y) {
            var context = canvas.getContext('2d');
            context.drawImage(image, 0, 0, image.width, image.height);
            var imageData = context.getImageData(0, 0, 1, 1);
            return [imageData.data[0], imageData.data[1], imageData.data[2], imageData.data[3]];
        }

        it('can call supportsImageBitmapOptions', function() {
            if (!supportsImageBitmapOptions) {
                return;
            }

            return Resource.supportsImageBitmapOptions()
                .then(function(result) {
                    expect(typeof result).toEqual('boolean');
                });
        });

        it('can load and decode an image', function() {
            if (!supportsImageBitmapOptions) {
                return;
            }

            return Resource.fetchImage({
                url: './Data/Images/Green.png',
                preferImageBitmap: true
            }).then(function(loadedImage) {
                expect(loadedImage.width).toEqual(1);
                expect(loadedImage.height).toEqual(1);
                expect(loadedImage).toBeInstanceOf(ImageBitmap);
            });
        });

        it('correctly flips image when ImageBitmapOptions are supported', function() {
            if (!supportsImageBitmapOptions) {
                return;
            }

            var loadedImage;

            return Resource.fetchImage({
                url: './Data/Images/BlueOverRed.png',
                flipY: true,
                preferImageBitmap: true
            }).then(function(image) {
                loadedImage = image;
                return Resource.supportsImageBitmapOptions();
            }).then(function(supportsImageBitmapOptions) {
                if (supportsImageBitmapOptions) {
                    expect(getColorAtPixel(loadedImage, 0, 0)).toEqual([255, 0, 0, 255]);
                } else {
                    expect(getColorAtPixel(loadedImage, 0, 0)).toEqual([0, 0, 255, 255]);
                }
            });
        });

        it('correctly loads image without flip when ImageBitmapOptions are supported', function() {
            if (!supportsImageBitmapOptions) {
                return;
            }

            var loadedImage;

            return Resource.fetchImage({
                url: './Data/Images/BlueOverRed.png',
                flipY: false,
                preferImageBitmap: true
             }).then(function(image) {
                loadedImage = image;
                return Resource.supportsImageBitmapOptions();
            }).then(function(supportsImageBitmapOptions) {
                if (supportsImageBitmapOptions) {
                    expect(getColorAtPixel(loadedImage, 0, 0)).toEqual([0, 0, 255, 255]);
                } else {
                    expect(getColorAtPixel(loadedImage, 0, 0)).toEqual([0, 0, 255, 255]);
                }
            });
        });

        it('does not use ImageBitmap when ImageBitmapOptions are not supported', function() {
            if (!supportsImageBitmapOptions) {
                return;
            }

            spyOn(Resource, 'supportsImageBitmapOptions').and.returnValue(when.resolve(false));
            spyOn(window, 'createImageBitmap').and.callThrough();

            return Resource.fetchImage({
                url: './Data/Images/Green.png',
                preferImageBitmap: true
            }).then(function(loadedImage) {
                expect(window.createImageBitmap).not.toHaveBeenCalledWith();
            });
        });

        it('rejects the promise when the image errors', function() {
            if (!supportsImageBitmapOptions) {
                return;
            }

            return Resource.fetchImage({
                url: 'http://example.invalid/testuri.png',
                preferImageBitmap: true
            })
                .then(function() {
                    fail('expected promise to reject');
                })
                .otherwise(function(error) {
                   expect(error).toBeInstanceOf(RequestErrorEvent);
                });
        });

        it('rejects the promise with extra error information when image errors and options.preferBlob is true', function() {
            if (!supportsImageBitmapOptions) {
                return;
            }

            // Force the fetching of a bad blob that is not an image to trigger the error
            spyOn(Resource.prototype, 'fetch').and.returnValue(when.resolve(new Blob([new Uint8Array([])], { type: 'text/plain' })));

            return Resource.fetchImage({
                url: 'http://example.invalid/testuri.png',
                preferImageBitmap: true,
                preferBlob: true
            })
                .then(function() {
                    fail('expected promise to reject');
                })
                .otherwise(function(error) {
                   expect(error.blob).toBeInstanceOf(Blob);
                });
        });
    });

    describe('fetchImage without ImageBitmap', function() {
        beforeAll(function() {
            // Force it to use the Image constructor since these specs all test
            // specific functionality of this code path. For example, the crossOrigin
            // restriction does not apply to images loaded with ImageBitmap.
            spyOn(Resource, 'supportsImageBitmapOptions').and.returnValue(when.resolve(false));
        });

        it('can load an image', function() {
            return Resource.fetchImage('./Data/Images/Green.png').then(function(loadedImage) {
                expect(loadedImage.width).toEqual(1);
                expect(loadedImage.height).toEqual(1);
            });
        });

        it('sets the crossOrigin property for cross-origin images', function() {
            var fakeImage = {};
            var imageConstructorSpy = spyOn(window, 'Image').and.returnValue(fakeImage);

            Resource.fetchImage('http://example.invalid/someImage.png');
            expect(imageConstructorSpy).toHaveBeenCalled();
            expect(fakeImage.crossOrigin).toEqual('');
        });

        it('does not set the crossOrigin property for non-cross-origin images', function() {
            var fakeImage = {};
            var imageConstructorSpy = spyOn(window, 'Image').and.returnValue(fakeImage);

            Resource.fetchImage('./someImage.png');
            expect(imageConstructorSpy).toHaveBeenCalled();
            expect(fakeImage.crossOrigin).toBeUndefined();
        });

        it('does not set the crossOrigin property for data URIs', function() {
            var fakeImage = {};
            var imageConstructorSpy = spyOn(window, 'Image').and.returnValue(fakeImage);

            Resource.fetchImage(dataUri);
            expect(imageConstructorSpy).toHaveBeenCalled();
            expect(fakeImage.crossOrigin).toBeUndefined();
        });

        it('resolves the promise when the image loads', function() {
            var fakeImage = {};
            spyOn(window, 'Image').and.returnValue(fakeImage);

            var success = false;
            var failure = false;
            var loadedImage;

            when(Resource.fetchImage(dataUri), function(image) {
                success = true;
                loadedImage = image;
            }, function() {
                failure = true;
            });

            // neither callback has fired yet
            expect(success).toEqual(false);
            expect(failure).toEqual(false);

            fakeImage.onload();
            expect(success).toEqual(true);
            expect(failure).toEqual(false);
            expect(loadedImage).toBe(fakeImage);
        });

        it('rejects the promise when the image errors', function() {
            var fakeImage = {};
            spyOn(window, 'Image').and.returnValue(fakeImage);

            var success = false;
            var failure = false;
            var loadedImage;

            when(Resource.fetchImage(dataUri), function(image) {
                success = true;
                loadedImage = image;
            }, function() {
                failure = true;
            });

            // neither callback has fired yet
            expect(success).toEqual(false);
            expect(failure).toEqual(false);

            fakeImage.onerror();
            expect(success).toEqual(false);
            expect(failure).toEqual(true);
            expect(loadedImage).toBeUndefined();
        });

        it('Calls loadWithXhr with blob response type if headers is set', function() {
            var expectedUrl = 'http://example.invalid/testuri.png';
            var expectedHeaders = {
                'X-my-header': 'my-value'
            };
            spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                expect(url).toEqual(expectedUrl);
                expect(headers).toEqual(expectedHeaders);
                expect(responseType).toEqual('blob');

                var binary = atob(dataUri.split(',')[1]);
                var array = [];
                for (var i = 0; i < binary.length; i++) {
                    array.push(binary.charCodeAt(i));
                }

                deferred.resolve(new Blob([new Uint8Array(array)], { type: 'image/png' }));
            });

            var testResource = new Resource({
                url: expectedUrl,
                headers: expectedHeaders
            });
            var promise = testResource.fetchImage();
            expect(promise).toBeDefined();

            return promise
                .then(function(image) {
                    expect(image).toBeDefined();
                });
        });

        it('Doesn\'t call loadWithXhr with blob response type if headers is set but is a data URI', function() {
            spyOn(Resource._Implementations, 'loadWithXhr').and.callFake(function(url, responseType, method, data, headers, deferred, overrideMimeType) {
                deferred.reject('this shouldn\'t happen');
            });

            spyOn(Resource._Implementations, 'createImage').and.callFake(function(url, crossOrigin, deferred) {
                expect(url).toEqual(dataUri);
            }).and.callThrough();

            var testResource = new Resource({
                url: dataUri,
                headers: {
                    'X-my-header': 'my-value'
                }
            });
            var promise = testResource.fetchImage();
            expect(promise).toBeDefined();

            return promise
                .then(function(image) {
                    expect(image).toBeDefined();
                });
        });

        describe('retries when Resource has the callback set', function() {
            it('rejects after too many retries', function() {
                var fakeImage = {};
                spyOn(window, 'Image').and.returnValue(fakeImage);

                var cb = jasmine.createSpy('retry').and.returnValue(true);

                var resource = new Resource({
                    url: 'http://example.invalid/image.png',
                    retryCallback: cb,
                    retryAttempts: 1
                });

                var promise = resource.fetchImage();

                expect(promise).toBeDefined();

                var success = false;
                var failure = false;
                promise.then(function() {
                    success = true;
                }).otherwise(function() {
                    failure = true;
                });

                expect(success).toBe(false);
                expect(failure).toBe(false);

                fakeImage.onerror('some error'); // This should retry
                expect(success).toBe(false);
                expect(failure).toBe(false);

                expect(cb.calls.count()).toEqual(1);
                var receivedResource = cb.calls.argsFor(0)[0];
                expect(receivedResource.url).toEqual(resource.url);
                expect(receivedResource._retryCount).toEqual(1);
                expect(cb.calls.argsFor(0)[1]).toEqual('some error');

                fakeImage.onerror(); // This fails because we only retry once
                expect(success).toBe(false);
                expect(failure).toBe(true);
            });

            it('rejects after callback returns false', function() {
                var fakeImage = {};
                spyOn(window, 'Image').and.returnValue(fakeImage);

                var cb = jasmine.createSpy('retry').and.returnValue(false);

                var resource = new Resource({
                    url: 'http://example.invalid/image.png',
                    retryCallback: cb,
                    retryAttempts: 2
                });

                var promise = resource.fetchImage();

                expect(promise).toBeDefined();

                var success = false;
                var failure = false;
                promise.then(function(value) {
                    success = true;
                }).otherwise(function(error) {
                    failure = true;
                });

                expect(success).toBe(false);
                expect(failure).toBe(false);

                fakeImage.onerror('some error'); // This fails because the callback returns false
                expect(success).toBe(false);
                expect(failure).toBe(true);

                expect(cb.calls.count()).toEqual(1);
                var receivedResource = cb.calls.argsFor(0)[0];
                expect(receivedResource.url).toEqual(resource.url);
                expect(receivedResource._retryCount).toEqual(1);
                expect(cb.calls.argsFor(0)[1]).toEqual('some error');
            });

            it('resolves after retry', function() {
                var fakeImage = {};
                spyOn(window, 'Image').and.returnValue(fakeImage);

                var cb = jasmine.createSpy('retry').and.returnValue(true);

                var resource = new Resource({
                    url: 'http://example.invalid/image.png',
                    retryCallback: cb,
                    retryAttempts: 1
                });

                var promise = resource.fetchImage();

                expect(promise).toBeDefined();

                var success = false;
                var failure = false;
                promise.then(function(value) {
                    success = true;
                }).otherwise(function(error) {
                    failure = true;
                });

                expect(success).toBe(false);
                expect(failure).toBe(false);

                fakeImage.onerror('some error'); // This should retry
                expect(success).toBe(false);
                expect(failure).toBe(false);

                expect(cb.calls.count()).toEqual(1);
                var receivedResource = cb.calls.argsFor(0)[0];
                expect(receivedResource.url).toEqual(resource.url);
                expect(receivedResource._retryCount).toEqual(1);
                expect(cb.calls.argsFor(0)[1]).toEqual('some error');

                fakeImage.onload();
                expect(success).toBe(true);
                expect(failure).toBe(false);
            });
        });
    });

    describe('loadWithXhr', function(){
        var loadWithXhr = function(options) {
            var resource = new Resource(options);
            return resource._makeRequest({
                responseType: options.responseType,
                overrideMimeType: options.overrideMimeType,
                method: defaultValue(options.method, 'GET'),
                data: options.data
            });
        };

        describe('data URI loading', function() {
            it('can load URI escaped text with default response type', function() {
                return loadWithXhr({
                    url : 'data:,Hello%2C%20World!'
                }).then(function(result) {
                    expect(result).toEqual('Hello, World!');
                });
            });

            it('can load URI escaped text with responseType=text', function() {
                return loadWithXhr({
                    url : 'data:,Hello%2C%20World!',
                    responseType : 'text'
                }).then(function(result) {
                    expect(result).toEqual('Hello, World!');
                });
            });

            it('can load Base64 encoded text with default response type', function() {
                return loadWithXhr({
                    url : 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ=='
                }).then(function(result) {
                    expect(result).toEqual('Hello, World!');
                });
            });

            it('can load Base64 encoded text with responseType=text', function() {
                return loadWithXhr({
                    url : 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ==',
                    responseType : 'text'
                }).then(function(result) {
                    expect(result).toEqual('Hello, World!');
                });
            });

            it('can load Base64 & URI encoded text with default responseType', function() {
                return loadWithXhr({
                    url : 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D'
                }).then(function(result) {
                    expect(result).toEqual('Hello, World!');
                });
            });

            it('can load Base64 & URI encoded text with responseType=text', function() {
                return loadWithXhr({
                    url : 'data:text/plain;base64,SGVsbG8sIFdvcmxkIQ%3D%3D',
                    responseType : 'text'
                }).then(function(result) {
                    expect(result).toEqual('Hello, World!');
                });
            });

            it('can load URI escaped HTML as text with default responseType', function() {
                return loadWithXhr({
                    url : 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E'
                }).then(function(result) {
                    expect(result).toEqual('<h1>Hello, World!</h1>');
                });
            });

            it('can load URI escaped HTML as text with responseType=text', function() {
                return loadWithXhr({
                    url : 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E',
                    responseType : 'text'
                }).then(function(result) {
                    expect(result).toEqual('<h1>Hello, World!</h1>');
                });
            });

            it('can load URI escaped text as JSON', function() {
                return loadWithXhr({
                    url : 'data:application/json,%7B%22key%22%3A%22value%22%7D',
                    responseType : 'json'
                }).then(function(result) {
                    expect(result.key).toEqual('value');
                });
            });

            it('can load Base64 encoded text as JSON', function() {
                return loadWithXhr({
                    url : 'data:application/json;base64,eyJrZXkiOiJ2YWx1ZSJ9',
                    responseType : 'json'
                }).then(function(result) {
                    expect(result.key).toEqual('value');
                });
            });

            it('can load URI escaped HTML as document', function() {
                return loadWithXhr({
                    url : 'data:text/html,%3Ch1%3EHello%2C%20World!%3C%2Fh1%3E',
                    responseType : 'document'
                }).then(function(result) {
                    expect(result.querySelector('h1')).not.toBeNull();
                });
            });

            var tile = 'data:;base64,rZ+P95jW+j0AAABAplRYwQAAAAAAAAAAFwEcw/RY1UUqDN/so3WJQETWnpq/aabA19EOeYzEh8D+6WPqtldYQYlaPeMGcRRAO+KDbxWIcsMAAAAAAAAAANsAAAD+N/03/mf/UQDq/f/+T/8foDBgL/8TACwAWP8H/xf/H/8b/yMAGP8r/wv/H/8P/w8ACP8PAEj/F/8TADwAIAAg/w8AMAAg/wcAGP8v/xf/J4gPIBOnIv8P/wf/FwAo/xfkM+MXACwACAAAAAgAGAAA/w//B/8P/wf/D/8XmgmZGf8XAAgAEBYZFQkAGAAQABj/DwAQAAgAEAAA/w8ACP8P/w8AIP8v2QD9DScR/w8AGAAQABAACAAQABwABAAIAAAAAAAI/xf/HwAQ/y8ADP8j/wf/D/8HRAlDEQAAABAAIP8fADAAFP8P/xP/H/8H5wf4Ag8TAAAABAAEAAD/D/8HAAAAAAAANhLKBf8HABAAIAAA/y95GHoIAAD/D/8DAAz/BwAAAAD/DwAAAAAAAAAAABD/Gz4Vwhb/G/8XAKz9t/4XAAj/D/0P/g8AAP0PAAAAAP4P/Q8AAAAAAAD+//8TABT/PwBA/y8AGP8n/w8AKAAYABAAAAAAAAAAAP8PABD/DwAQAAD/DwAIAAgAAP8PABD9//4P/Q/+H/8P/Q/+EwAYAAz/H/8PABj9H/43ABAACP1PAADWBdUF/gMADv0R/g8AEP8LdwCHBwAI/wsACP8HAAAACAAMAAQACP8DAAwACP8P/wcAIP8PABAACAAE/xsAAP8HABj/H/8HABT/CwAQABAAAL4WFwSlAv8PABAAAAAIAAh0A4wEAAD/B/8P/w8AAAAQAAgAEAAQ/wcAAAAI9wQHCwAAABAAAAgI+Af/BwAA/wcAEAAA/xf/BwAYAAAAGAAAAAD/DwAQuw/4AMQO/w8AGAAAAAAAEP8HAAT/E/8HABAAIFESUgoACP8D/wsAFP8T/w//D/8H1hvVA/8PACAADAAEAAgABAAQ/xMACP8HYREzDGkK/w8AHP8j/w8AAAAY/w8AMP8P5hoaHf8PACD/BwAQAAQ5HjoS/yf/HwAU/xv/B/8P/w8AEP8PACAAEAAgAAj/G7ItTh7/CwAIAAQACP8f/w//D/8P/w//DwAQ/w//DwAAAEAAEAAQABAAEP8TAAT/B/8H/wcACP8HABj/JwAIAAD/D/8P/w//D/8PAAD/DwAA/w8AAP8PAAD/D/8H/wcAQAAA/w8AAAAA/w8ABAAAABT/JwAIAAD/BwAYABD/D/83ABBwbM1nul3BXggB7QCqADoARH8JagIPNQkNGk5YCQunTVhLcUvMAAsBWAAbAKkArQCgAckA0AAXABwAGwCCAL4AHgqlCgIWfxVhAGAA3wByAKTgYdIxDm8AIwAxAIYAQwAYqNmncha+DYckCgDaADcAiwCuAs4GFAUvBlcI0vb59iAAYQAHAHbgr986A34DMgGBCbIAOABKAH4AlwDMABUB4QC4AdkBDm0OV83CDQCdAPsAPgAuAdwA6AAzAD4W3QmLC+Rs3WzvANwAgQPOCxEAuwomAFUAXoP9gygAKiIvEVYKWQUlFJsAWgQUBrYPnGdRNw9MQgAVAGAAlAA4AHMAkgAXASYApJgDkeZO+VX8BVQRkxb8w2vEpQB0AEcAHgCOAIAAWwCgAFMACQApAE0AKAA5AHTgz99zAD4AtABVAK4ApwCfAD8ApgAeAGMAagBYAD8AwwBMACAAEwBkAAgBLQCKZhlmQgA+AJxXuxLVRE8ATQCAAfIkBxvDA6kIEQA5AAIAPgA1ACwAEAAcADIAFwCSALkAIgGlAZwAnADrAJkAKQAJAMYAcwAaAG0ADwAFALcBuACSAQAAAAAAAAAAAwAAAAMAAwAAAAMABAACAAAABgAAAAAABQAIAAEAAgAIAAAABwABAAkABwAAAAAAAAADAAoACAABAAoAAgAEAAoACAAAAAAACgAAAAQAAQALAAIAAQAAAAUAAQAAAAYABgABAAgAAAAJAAAAAQAKAAMACgACAAkACwAJAAAACgADAAEAAQAOAAwAAAAPAAIADwABAAAAAQAAABEAEAARAAEAFAAQAAAAAQARAAIAAgADAAAAAwABAAAABAABAAMAAAAGAAcABgABAAAABAAHAAAAAQAIAAIAAAAKAAsACgABAAQACwANAAEAAAAOAA0AAQACAA4AAAAAAA8AEQACAA8ADwABAAMAEgACABEAEgATAAAAAQADABMAFAAAAAIAAQAVABYAFgAXAAEAAgABAAAAAgAAAAIAAQADABkABAACAAAAAQAHAAUABwABAAAABwAIAAEACQAHAAAAAAAJAAAACgAAAAQAAgALAAUACwADAAEAAAAPAA4AAQAOAAUADwABABIAAgAAAAIAEwACAAEABgADAAIAEwAAABMAFAACAAEAAgAAAAAAAgAEAAYAAwAEAAEACAACAAYACAAHAAAACQAAAAQAAgABAAoAAAAKAAAACwAOAAAABQAMAAMAAgAMAAEADwAQAAAAAgAQAAEAAAACABIAEgAUAAEAAAADAAIAAAAEAAIABAABAAUABQABAAYAAAAIAAcABwACAAEACAAAAAAAAwACAAoAAQAMAAoAAAAMAA0AAQANAAIAAAANAAAAAAASAA8AAwABAA8ADwAEAAIAEQASAAEAAAASAAAAEwACABUAAwABABMAAAAEAAYAAQACAAQAAAAAAAgACAABAAMABwACAAgAAgAHAAAAAQAIAAoACgAAAAIADAABAAsADQABAAwADQAOAAAAAgAOAAAAAQAPAAAAAwABABAAEgAAABIABAASAAEAEwAAAAIAFQABABQAAQAAAAMAAwAAAAcABAACAAEAAQAFAAcABQAAAAAABwAAAAkAAQAIAAIAAwAIAAAAAQAJAAUAAgAAAAsACwABAAAADAABAA4ADwAOAAEAAAASABAAAgABABAAAQARABIAAAASAAIAEgABAAAAFQATAAEAAAAEAAAABgAHAAUAAQAFAAcABQACAAQABwAAAAIACAAKAAEACgAAAAIAAQALAAAAAQAMAA0ADgABAA0AAAACAA8AAQAPAAAADQABABAAEQASAA0AEgABAA0AAQASAAAAAgABAAAAAQAEAAMAAAAFAAAAAAAHAAMAAQAIAAcAAgAHAAQACAAAAAoAAQAJAAAACgADAAEACwAAAA0ADAADAAAAAQACAA0ADwAOAAIAAgAQAA8AAgAAABEAEwARAAAAAQASAAIAAQAAABUAFAAVAAEAAQAAABUAFQABACkAAgAAAAAAAgAEAAUAAwAEAAAAAQAFAAIABwAAAAcABwABAAQAAAAAAAoAAwAKAAEACgAMAAIAAAANAAwADQABAAMAAAACAA0ADQAQAAEAAAAAAAQABgAEAAEAAwACAAQAAQAFAAYABQAAAAAABwADAAIABwAAAAoAAQAIAAIAAAAOAAsACwACAAEADQAOAAEAAAAAAA8ADwABAA4AAgAPAAAAAQAQAAQAAAATABAAEAADAAEAPAA7ABMAAQA8ABMAPAABAAAAAQACAAAAAwAFAAEABQAGAAAAAAAHAAIAAwAHAAEACAAAAAAACAACAAoAAQAEAAoACAAMAAAAAQADAAkADAANAAkADgABAA0ADwAAAA8AAgAPAAAAAQAQAAIAAAASAAAAEwAUAAEABAATAAIAFAAVAAEAAQAVAAAAAwACAAEAAQAAAAQAAQAGAAQAAAAHAAAACAACAAcAAQAIAAMAAAAKAAgACAADAAEADAAKAAEADAAAAAwAAgAAAA4AAgAOAAEAAAARAA4ADgADAAEAAAAAABMAAwACABMAEwABABIAAAAVABMAAQATAAIAAgAAAAAABAABAAMABQACAAQABgAIAAUAAAADAAYABgAJAAEAAAACAAoACQABAAoAAAAKAAAAAwALAAAAAQAMAAMAAgAMAA4ADgAQAAIAAAADABEAEQAQAAEAAAAAABIAFAACABIAAwASAAEAFAAVAAIAAgAAAAIAAwAWAAAAAAADAAUAAgABAAUAAAAFAAQABQABAAcAAAAKAAgACAACAAEACgABAAsACwABAAAACgAMAAEACgAAAAAAAQAPAAwAAwACAAwAAAAQAAIAEAABABEAAQATABEAAQAAAAAAAAAEAAIAAAAXAAUABQACAAEABAAFAAYABgAHAAQAAAAIAAkACQAAAAIACwABAAoACwAMAAEAAQAMAAAAEAABAA0AEAAOAAAAAgARAAEAAAAQABEAAQARAAAAAgABAAAAAgATAAAAAwAAAAMABAAAAAAABgAEAAIAAQADAAYABABCAAAAQwAFAAAABAAGAAIAAQAGABkABAAAAAQAVQAFAAMABQBVAAEAAAAHAAUABQACAAEABwABAAAAAgADAFoAAgAAAAIAAQADAFsAWwAEAFkAWQAEAFcABABYAFcAWAAGAEkABgBIAEkAMwBIAAUAHQAzAAUAWwBaAAEAAQBaAAAAAABcAF4AXAABAAIAAAACAGAAXwBgAAIAAAAAAAMAAgADAGIAYgB0AAIAAAACAAMAdQABAAMAAgABAAAAAgAAAAIAAwB3AHgAAQADAAAAAQAEAHkAAAADAAIAAwABAAAAewCPAAMAAwCPAAIAAAADAAAAkQCSAAQABAACAAMAAQAEAJIAAQAAAAMAkwCmAAIApgABAAIApgClAAEAIQAAACEAIQABAAAAAQA4ACIAAAACAAMAOQACADgAOAACAAAAAAACAAQABAADAAEAAwAAAAIAAAAEAAMAAwACAAEABAABAAAAPQAFAD8AAQAAAAYAQAAGAAEAAAAEAAAABAAFAAAABgAHAAIAAQAGAAMABwAAAAMAAQAEAAMAvQC+AAQABAABAL0ABAC+AAIAvgAGAAIAAAAGAAcAAQAHALsAuwAHAL8ABgABAAAABwBaAEYAWgAHAAEAAQACAAAAvgADAL0AAQADAL4AAgBcAFsAAQCZAAIAAgCZAFwArACZAAEAvgCsAAEAXACZAJgAwAAAAMMAwQAFAAAAAgDCAAEA2AACANYAxAACANgAEQAAAAEAnwCoANUA2QAFANoAzgDLAMkApwCmAKMAqgCsAK0AqwACAAAAAQAEABEAAAAEAK4ADAAkAMYAxQDIAMIAwQC/ALsAugC5ALIAsAC8AL0AAgAAAJ8ArgA=';

            it('can load Base64 encoded data as arraybuffer', function() {
                return loadWithXhr({
                    url : tile,
                    responseType : 'arraybuffer'
                }).then(function(result) {
                    expect(result.byteLength).toEqual(3914);
                });
            });

            var image = '';

            it('can load Base64 encoded data as blob', function() {
                return loadWithXhr({
                    url : image,
                    responseType : 'blob'
                }).then(function(result) {
                    expect(result.type).toEqual('image/png');

                    var blobUrl = URL.createObjectURL(result);

                    return Resource.fetchImage(blobUrl).then(function(image) {
                        expect(image.width).toEqual(24);
                        expect(image.height).toEqual(24);
                    });
                });
            });

            xit('can support 2xx HTTP status (other than 200)', function() {
                return loadWithXhr({
                    method : 'POST',
                    url : 'http://jsonplaceholder.typicode.com/posts',
                    data : {
                        title : 'foo',
                        body : 'bar',
                        userId : 1
                    }
                }).then(function(result) {
                    expect(JSON.parse(result).id).toEqual(101);
                });
            });
        });

        describe('URL loading using XHR', function() {
            describe('returns a promise that rejects when the request', function() {
                it('results in an HTTP status code greater than or equal to 300', function() {
                    return loadWithXhr({
                        url : 'http://example.invalid'
                    }).then(function() {
                        fail('expected promise to reject');
                    }).otherwise(function(err) {
                        expect(err).toBeInstanceOf(RequestErrorEvent);
                    });
                });

                it('loads an invalid JSON string response with a json responseType', function() {
                    return loadWithXhr({
                        url : 'Data/htmlString.txt',
                        responseType : 'json'
                    }).then(function() {
                        fail('expected promise to reject');
                    }).otherwise(function(err) {
                        expect(err).toBeDefined();
                        expect(err).toBeInstanceOf(Error);
                    });
                });
            });

            describe('returns a promise that resolves when the request loads', function() {
                it('a non-null response with default responseType', function() {
                    return loadWithXhr({
                        url : 'Data/Models/Box/ReadMe.txt'
                    }).then(function(result) {
                        expect(result).toBe('CesiumBoxTest-NoTechnique.gltf is a modified glTF that has techniques, shaders & programs removed.');
                    });
                });

                it('a non-null response with a browser-supported responseType', function() {
                    return loadWithXhr({
                        url : 'Data/Models/Box/ReadMe.txt',
                        responseType : 'text'
                    }).then(function(result) {
                        expect(result).toBe('CesiumBoxTest-NoTechnique.gltf is a modified glTF that has techniques, shaders & programs removed.');
                    });
                });

                it('a valid JSON string response as JSON with a json responseType', function() {
                    return loadWithXhr({
                        url : 'Data/jsonString.txt',
                        responseType : 'json'
                    }).then(function(result) {
                        expect(result).toEqual(jasmine.objectContaining({hello : 'world'}));
                    });
                });
            });
        });

        describe('URL loading using mocked XHR', function() {
            var fakeXHR;

            beforeEach(function() {
                fakeXHR = jasmine.createSpyObj('XMLHttpRequest', ['send', 'open', 'setRequestHeader', 'abort', 'getAllResponseHeaders']);
                fakeXHR.simulateError = function() {
                    fakeXHR.response = '';
                    if (typeof fakeXHR.onerror === 'function') {
                        fakeXHR.onerror();
                    }
                };
                fakeXHR.simulateHttpResponse = function(statusCode, response) {
                    fakeXHR.status = statusCode;
                    fakeXHR.response = response;
                    if (typeof fakeXHR.onload === 'function') {
                        fakeXHR.onload();
                    }
                };
                fakeXHR.simulateResponseXMLLoad = function(responseXML) {
                    fakeXHR.status = 200;
                    fakeXHR.responseXML = responseXML;
                    if (typeof fakeXHR.onload === 'function') {
                        fakeXHR.onload();
                    }
                };
                fakeXHR.simulateResponseTextLoad = function(responseText) {
                    fakeXHR.simulateHttpResponse(200, responseText);
                };

                spyOn(window, 'XMLHttpRequest').and.returnValue(fakeXHR);
            });

            describe('returns a promise that rejects when the request', function() {
                it('errors', function() {
                    var promise = loadWithXhr({
                        url : 'http://example.invalid'
                    });

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    fakeXHR.simulateError();
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeInstanceOf(RequestErrorEvent);
                });

                it('results in an HTTP status code less than 200', function() {
                    var promise = loadWithXhr({
                        url : 'http://example.invalid'
                    });

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    fakeXHR.simulateHttpResponse(199);
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeInstanceOf(RequestErrorEvent);
                });

                it('is an image with status code 204 with preferImageBitmap', function() {
                    if (!supportsImageBitmapOptions) {
                        return;
                    }

                    var promise = Resource.fetchImage({
                        url: './Data/Images/Green.png',
                        preferImageBitmap: true
                    });

                    expect(promise).toBeDefined();

                    var resolved = false;
                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolved = true;
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    fakeXHR.simulateHttpResponse(204);
                    expect(resolved).toBe(false);
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeDefined();
                });

                it('resolves undefined for status code 204', function() {
                    var promise = loadWithXhr({
                        url : 'http://example.invalid'
                    });

                    expect(promise).toBeDefined();

                    var resolved = false;
                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolved = true;
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    fakeXHR.simulateHttpResponse(204);
                    expect(resolved).toBe(true);
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();
                });
            });

            describe('returns a promise that resolves when the request loads', function() {
                it('a null response with a \'\' responseType and non-null responseXML with child nodes', function() {
                    var promise = loadWithXhr({
                        url : 'http://example.invalid',
                        responseType : ''
                    });

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    var responseXML = {
                        hasChildNodes : jasmine.createSpy('hasChildNodes').and.returnValue(true)
                    };
                    fakeXHR.simulateResponseXMLLoad(responseXML);
                    expect(resolvedValue).toEqual(responseXML);
                    expect(rejectedError).toBeUndefined();
                });

                it('a null response with a document responseType and non-null responseXML with child nodes', function() {
                    var promise = loadWithXhr({
                        url : 'http://example.invalid',
                        responseType : 'document'
                    });

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    var responseXML = {
                        hasChildNodes : jasmine.createSpy('hasChildNodes').and.returnValue(true)
                    };
                    fakeXHR.simulateResponseXMLLoad(responseXML);
                    expect(resolvedValue).toEqual(responseXML);
                    expect(rejectedError).toBeUndefined();
                });

                it('a null response with a \'\' responseType and non-null responseText', function() {
                    var promise = loadWithXhr({
                        url : 'http://example.invalid',
                        responseType : ''
                    });

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    var responseText = 'hello world';
                    fakeXHR.simulateResponseTextLoad(responseText);
                    expect(resolvedValue).toEqual(responseText);
                    expect(rejectedError).toBeUndefined();
                });

                it('a null response with a text responseType and non-null responseText', function() {
                    var promise = loadWithXhr({
                        url : 'http://example.invalid',
                        responseType : 'text'
                    });

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    var responseText = 'hello world';
                    fakeXHR.simulateResponseTextLoad(responseText);
                    expect(resolvedValue).toEqual(responseText);
                    expect(rejectedError).toBeUndefined();
                });
            });

            describe('retries when Resource has the callback set', function() {
                it('rejects after too many retries', function() {
                    var cb = jasmine.createSpy('retry').and.returnValue(true);

                    var resource = new Resource({
                        url : 'http://example.invalid',
                        retryCallback: cb,
                        retryAttempts: 1
                    });

                    var promise = loadWithXhr(resource);

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    fakeXHR.simulateError(); // This should retry
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    expect(cb.calls.count()).toEqual(1);
                    var receivedResource = cb.calls.argsFor(0)[0];
                    expect(receivedResource.url).toEqual(resource.url);
                    expect(receivedResource._retryCount).toEqual(1);
                    expect(cb.calls.argsFor(0)[1] instanceof RequestErrorEvent).toBe(true);

                    fakeXHR.simulateError(); // This fails because we only retry once
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeInstanceOf(RequestErrorEvent);
                });

                it('rejects after callback returns false', function() {
                    var cb = jasmine.createSpy('retry').and.returnValue(false);

                    var resource = new Resource({
                        url : 'http://example.invalid',
                        retryCallback: cb,
                        retryAttempts: 2
                    });

                    var promise = loadWithXhr(resource);

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    fakeXHR.simulateError(); // This fails because the callback returns false
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeInstanceOf(RequestErrorEvent);

                    expect(cb.calls.count()).toEqual(1);
                    var receivedResource = cb.calls.argsFor(0)[0];
                    expect(receivedResource.url).toEqual(resource.url);
                    expect(receivedResource._retryCount).toEqual(1);
                    expect(cb.calls.argsFor(0)[1] instanceof RequestErrorEvent).toBe(true);
                });

                it('resolves after retry', function() {
                    var cb = jasmine.createSpy('retry').and.returnValue(true);

                    var resource = new Resource({
                        url : 'http://example.invalid',
                        retryCallback: cb,
                        retryAttempts: 1
                    });

                    var promise = loadWithXhr(resource);

                    expect(promise).toBeDefined();

                    var resolvedValue;
                    var rejectedError;
                    promise.then(function(value) {
                        resolvedValue = value;
                    }).otherwise(function (error) {
                        rejectedError = error;
                    });

                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    fakeXHR.simulateError(); // This should retry
                    expect(resolvedValue).toBeUndefined();
                    expect(rejectedError).toBeUndefined();

                    expect(cb.calls.count()).toEqual(1);
                    var receivedResource = cb.calls.argsFor(0)[0];
                    expect(receivedResource.url).toEqual(resource.url);
                    expect(receivedResource._retryCount).toEqual(1);
                    expect(cb.calls.argsFor(0)[1] instanceof RequestErrorEvent).toBe(true);

                    fakeXHR.simulateHttpResponse(200, 'OK');
                    expect(resolvedValue).toBeDefined();
                    expect(rejectedError).toBeUndefined();
                });
            });
        });
    });

    describe('fetchJsonp', function() {

        it('returns a promise that resolves when the request loads', function() {
            var testUrl = 'http://example.invalid/testuri';
            spyOn(Resource._Implementations, 'loadAndExecuteScript').and.callFake(function(url, name, deffered) {
                expect(url).toContain(testUrl);
                expect(name).toContain('loadJsonp');
                expect(deffered).toBeDefined();
            });
            Resource.fetchJsonp(testUrl);
        });

        it('returns a promise that rejects when the request errors', function() {
            var testUrl = 'http://example.invalid/testuri';
            return Resource.fetchJsonp(testUrl).otherwise(function(error) {
                expect(error).toBeDefined();
            });
        });

        it('Uses callback name specified in options', function() {
            var testUrl = 'test';
            var options = {
                callbackParameterName: 'testCallback'
            };
            spyOn(Resource._Implementations, 'loadAndExecuteScript').and.callFake(function(url, functionName, deferred) {
                expect(url).toContain('callback=loadJsonp');
            });
            Resource.fetchJsonp(testUrl, options);
        });

        describe('retries when Resource has the callback set', function() {
            it('rejects after too many retries', function() {
                //var cb = jasmine.createSpy('retry').and.returnValue(true);
                var cb = jasmine.createSpy('retry').and.callFake(function(resource, error) {
                    return true;
                });

                var lastDeferred;
                spyOn(Resource._Implementations, 'loadAndExecuteScript').and.callFake(function(url, functionName, deferred) {
                    lastDeferred = deferred;
                });

                var resource = new Resource({
                    url: 'http://example.invalid',
                    retryCallback: cb,
                    retryAttempts: 1
                });

                var promise = resource.fetchJsonp();

                expect(promise).toBeDefined();

                var resolvedValue;
                var rejectedError;
                promise.then(function(value) {
                    resolvedValue = value;
                }).otherwise(function(error) {
                    rejectedError = error;
                });

                expect(resolvedValue).toBeUndefined();
                expect(rejectedError).toBeUndefined();

                lastDeferred.reject('some error'); // This should retry
                expect(resolvedValue).toBeUndefined();
                expect(rejectedError).toBeUndefined();

                expect(cb.calls.count()).toEqual(1);
                var receivedResource = cb.calls.argsFor(0)[0];
                expect(receivedResource.url).toEqual(resource.url);
                expect(receivedResource._retryCount).toEqual(1);
                expect(cb.calls.argsFor(0)[1]).toEqual('some error');

                lastDeferred.reject('another error'); // This fails because we only retry once
                expect(resolvedValue).toBeUndefined();
                expect(rejectedError).toEqual('another error');
            });

            it('rejects after callback returns false', function() {
                var cb = jasmine.createSpy('retry').and.returnValue(false);

                var lastDeferred;
                spyOn(Resource._Implementations, 'loadAndExecuteScript').and.callFake(function(url, functionName, deferred) {
                    lastDeferred = deferred;
                });

                var resource = new Resource({
                    url: 'http://example.invalid',
                    retryCallback: cb,
                    retryAttempts: 2
                });

                var promise = resource.fetchJsonp();

                expect(promise).toBeDefined();

                var resolvedValue;
                var rejectedError;
                promise.then(function(value) {
                    resolvedValue = value;
                }).otherwise(function(error) {
                    rejectedError = error;
                });

                expect(resolvedValue).toBeUndefined();
                expect(rejectedError).toBeUndefined();

                lastDeferred.reject('some error'); // This fails because the callback returns false
                expect(resolvedValue).toBeUndefined();
                expect(rejectedError).toEqual('some error');

                expect(cb.calls.count()).toEqual(1);
                var receivedResource = cb.calls.argsFor(0)[0];
                expect(receivedResource.url).toEqual(resource.url);
                expect(receivedResource._retryCount).toEqual(1);
                expect(cb.calls.argsFor(0)[1]).toEqual('some error');
            });

            it('resolves after retry', function() {
                var cb = jasmine.createSpy('retry').and.returnValue(true);

                var lastDeferred;
                var lastUrl;
                spyOn(Resource._Implementations, 'loadAndExecuteScript').and.callFake(function(url, functionName, deferred) {
                    lastUrl = url;
                    lastDeferred = deferred;
                });

                var resource = new Resource({
                    url: 'http://example.invalid',
                    retryCallback: cb,
                    retryAttempts: 1
                });

                var promise = resource.fetchJsonp();

                expect(promise).toBeDefined();

                var resolvedValue;
                var rejectedError;
                promise.then(function(value) {
                    resolvedValue = value;
                }).otherwise(function(error) {
                    rejectedError = error;
                });

                expect(resolvedValue).toBeUndefined();
                expect(rejectedError).toBeUndefined();

                lastDeferred.reject('some error'); // This should retry
                expect(resolvedValue).toBeUndefined();
                expect(rejectedError).toBeUndefined();

                expect(cb.calls.count()).toEqual(1);
                var receivedResource = cb.calls.argsFor(0)[0];
                expect(receivedResource.url).toEqual(resource.url);
                expect(receivedResource._retryCount).toEqual(1);
                expect(cb.calls.argsFor(0)[1]).toEqual('some error');

                var uri = new Uri(lastUrl);
                var query = queryToObject(uri.query);
                window[query.callback]('something good');
                expect(resolvedValue).toEqual('something good');
                expect(rejectedError).toBeUndefined();
            });
        });
    });

});
